﻿/// ****************************************************************************
/// Author:	        Robin Zhu
/// Create Date:	2012-4-3
/// Purpose: 		扩展成员身份认证程序，使用数据库中的数据进行用户身份验证
/// ****************************************************************************
/// Modify By		Date			Remark
/// Robin           2012-6-4        增加AddUserToApp方法
/// Robin           2012-6-20       修复GetPassword方法
/// Robin           2014-2-21       在membership update中增加更新IsStaff, IsApproved, IsLockedOut
/// ****************************************************************************

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Security;
using System.Collections.Specialized;
using System.Configuration.Provider;
using System.Text.RegularExpressions;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Configuration;
using System.Web.Configuration;
using System.Web;

namespace RB.Web.Security
{
    /// <summary>
    /// 基于数据库的成员身份提供程序
    /// </summary>
    public class VarMembershipProvider : MembershipProvider
    {
        private const string EmailRegexPattern = @"^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$";
        private const string EventSource = "VarMembershipProvider";
        private const string EventLogName = "Application";

        /// <summary>
        /// Used when determining encryption key values.
        /// </summary>
        private MachineKeySection _machineKey;

        #region 自定义属性

        private int _lenghtOfRandomPassword = 8;

        public int LenghtOfRandomPassword
        {
            get { return _lenghtOfRandomPassword; }
        }

        private bool _writeExceptionsToEventLog = false;

        public bool WriteExceptionsToEventLog
        {
            get
            {
                return _writeExceptionsToEventLog;
            }
        }

        private string _serviceType = null;
        private IMembershipService _membershipService = null;

        #endregion

        #region System.Web.Security.MembershipProvider 属性.

        private string _applicationName = System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath;
        private bool _enablePasswordReset = false;
        private bool _enablePasswordRetrieval = false;
        private int _maxInvalidPasswordAttempts = int.MaxValue;
        private int _minRequiredNonAlphanumericCharacters = 0;
        private int _minRequiredPasswordLength = 0;
        private int _passwordAttemptWindow = 10;
        private MembershipPasswordFormat _passwordFormat = MembershipPasswordFormat.Clear;
        private string _passwordStrengthRegularExpression = null;
        private bool _requiresQuestionAndAnswer = true;
        private bool _requiresUniqueEmail = true;

        /// <summary>
        /// 使用自定义成员资格提供程序的应用程序的名称。
        /// </summary>
        public override string ApplicationName
        {
            get
            {
                return _applicationName;
            }
            set
            {
                _applicationName = value;
            }
        }

        /// <summary>
        /// 指示成员资格提供程序是否配置为允许用户重置其密码。
        /// </summary>
        public override bool EnablePasswordReset
        {
            get { return _enablePasswordReset; }
        }

        /// <summary>
        /// 指示成员资格提供程序是否配置为允许用户检索其密码
        /// </summary>
        public override bool EnablePasswordRetrieval
        {
            get { return _enablePasswordRetrieval; }
        }

        /// <summary>
        /// 获取锁定成员资格用户之前允许的无效密码或无效密码提示问题答案尝试次数
        /// </summary>
        public override int MaxInvalidPasswordAttempts
        {
            get { return _maxInvalidPasswordAttempts; }
        }

        /// <summary>
        /// 获取有效密码中必须包含的最少特殊字符数
        /// </summary>
        public override int MinRequiredNonAlphanumericCharacters
        {
            get { return _minRequiredNonAlphanumericCharacters; }
        }

        public override int MinRequiredPasswordLength
        {
            get { return _minRequiredPasswordLength; }
        }

        /// <summary>
        /// 对未能提供有效密码或密码答案的连续尝试次数进行跟踪的时间长度（以分钟为单位）。
        /// 默认值为 10 分钟。
        /// 如果当前失败尝试和上次失败尝试之间的间隔大于 PasswordAttemptWindow 属性设置，则每个失败尝试均被视为第一次失败尝试。
        /// </summary>
        public override int PasswordAttemptWindow
        {
            get { return _passwordAttemptWindow; }
        }

        /// <summary>
        /// 获取一个值，该值指示在成员资格数据存储区中存储密码的格式
        /// </summary>
        public override MembershipPasswordFormat PasswordFormat
        {
            get { return _passwordFormat; }
        }

        /// <summary>
        /// 获取用于计算密码的正则表达式
        /// </summary>
        public override string PasswordStrengthRegularExpression
        {
            get { return _passwordStrengthRegularExpression; }
        }

        /// <summary>
        /// 获取一个值，该值指示成员资格提供程序是否配置为要求用户在进行密码重置和检索时回答密码提示问题
        /// </summary>
        public override bool RequiresQuestionAndAnswer
        {
            get { return _requiresQuestionAndAnswer; }
        }

        /// <summary>
        /// 获取一个值，指示成员资格提供程序是否配置为要求每个用户名具有唯一的电子邮件地址
        /// </summary>
        public override bool RequiresUniqueEmail
        {
            get { return _requiresUniqueEmail; }
        }

        #endregion System.Web.Security.MembershipProvider properties.

        /// <summary>
        /// A helper function to retrieve config values from the configuration file.
        /// </summary>
        /// <param name="configValue"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        private void GetConfigValue(string configValue, ref string defaultValue)
        {
            if (!String.IsNullOrEmpty(configValue))
            {
                defaultValue = configValue;
            }
        }

        private void GetConfigValue(string configValue, ref bool defaultValue)
        {
            if (!String.IsNullOrEmpty(configValue))
            {
                defaultValue = Convert.ToBoolean(configValue);
            }
        }

        private void GetConfigValue(string configValue, ref int defaultValue)
        {
            if (!String.IsNullOrEmpty(configValue))
            {
                defaultValue = Convert.ToInt32(configValue);
            }
        }

        private void GetConfigValue(string configValue, ref MembershipPasswordFormat defaultValue)
        {
            if (!String.IsNullOrEmpty(configValue))
            {
                switch (configValue)
                {
                case "Hashed":
                    defaultValue = MembershipPasswordFormat.Hashed;
                    break;
                case "Encrypted":
                    defaultValue = MembershipPasswordFormat.Encrypted;
                    break;
                case "Clear":
                    defaultValue = MembershipPasswordFormat.Clear;
                    break;
                default:
                    throw new ProviderException("Password format not supported.");
                }
            }
        }

        private void InitializeConfig(NameValueCollection config)
        {
            GetConfigValue(config["applicationName"], ref _applicationName);
            GetConfigValue(config["enablePasswordReset"], ref _enablePasswordReset);
            GetConfigValue(config["enablePasswordRetrieval"], ref _enablePasswordRetrieval);
            GetConfigValue(config["maxInvalidPasswordAttempts"], ref _maxInvalidPasswordAttempts);
            GetConfigValue(config["minRequiredNonAlphanumericCharacters"], ref _minRequiredNonAlphanumericCharacters);
            GetConfigValue(config["minRequiredPasswordLength"], ref _minRequiredPasswordLength);
            GetConfigValue(config["passwordAttemptWindow"], ref _passwordAttemptWindow);
            GetConfigValue(config["passwordFormat"], ref _passwordFormat);
            GetConfigValue(config["passwordStrengthRegularExpression"], ref _passwordStrengthRegularExpression);
            GetConfigValue(config["requiresQuestionAndAnswer"], ref _requiresQuestionAndAnswer);
            GetConfigValue(config["requiresUniqueEmail"], ref _requiresUniqueEmail);

            // 自定义属性
            GetConfigValue(config["lenghtOfRandomPassword"], ref _lenghtOfRandomPassword);
            GetConfigValue(config["writeExceptionsToEventLog"], ref _writeExceptionsToEventLog);

            GetConfigValue(config["serviceType"], ref _serviceType);
        }

        public override void Initialize(string name, NameValueCollection config)
        {
            if (config == null)
                throw new ArgumentNullException("config");

            if (string.IsNullOrEmpty(name))
                name = "VarMembershipProvider";

            if (String.IsNullOrEmpty(config["description"]))
            {
                config.Remove("description");
                config.Add("description", "RayBullet Studio Var Membership provider");
            }

            base.Initialize(name, config);

            InitializeConfig(config);

            // Get encryption and decryption key information from the configuration.
            Configuration cfg =
              WebConfigurationManager.OpenWebConfiguration(System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath);
            _machineKey = (MachineKeySection)cfg.GetSection("system.web/machineKey");

            if (_machineKey.ValidationKey.Contains("AutoGenerate"))
                if (PasswordFormat != MembershipPasswordFormat.Clear)
                    throw new ProviderException("Hashed or Encrypted passwords " +
                                                "are not supported with auto-generated keys.");

            _membershipService = (IMembershipService)Activator.CreateInstance(Type.GetType(_serviceType));
        }

        public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
        {
            ValidatePasswordEventArgs args =
              new ValidatePasswordEventArgs(username, password, true);

            OnValidatingPassword(args);

            if (args.Cancel)
            {
                status = MembershipCreateStatus.InvalidPassword;
                return null;
            }

            if (RequiresUniqueEmail)
            {
                if (string.IsNullOrEmpty(email) || !Regex.IsMatch(email, EmailRegexPattern))
                {
                    status = MembershipCreateStatus.InvalidEmail;
                    return null;
                }
                else if (GetUserNameByEmail(email) != "")
                {
                    status = MembershipCreateStatus.DuplicateEmail;
                    return null;
                }
            }

            VarMembershipUser u = (VarMembershipUser)GetUser(username, false);

            if (u == null)
            {
                try
                {
                    if (providerUserKey == null)
                    {
                        providerUserKey = Guid.NewGuid();
                    }
                    password = EncodePassword(password);

                    UserAccount userAccount = new UserAccount()
                    {
                        Id = (Guid)providerUserKey,
                        UserName = username,
                        Password = password,
                        Email = email,
                        PasswordQuestion = passwordQuestion,
                        PasswordAnswer = passwordAnswer,
                        IsApproved = isApproved,
                        IsDisabled = false,
                        IsBuiltin = false,
                        IsLockedOut = false,
                        CreateBy = HttpContext.Current.User.Identity.Name,
                        CreateTime = DateTime.Now
                    };
                    bool success = _membershipService.CreateUser(userAccount);
                    if (success)
                    {
                        status = MembershipCreateStatus.Success;

                        return new VarMembershipUser(this.Name, userAccount);
                    }
                    else
                    {
                        status = MembershipCreateStatus.UserRejected;
                    }
                }
                catch (Exception ex)
                {
                    if (WriteExceptionsToEventLog)
                    {
                        WriteToEventLog(ex, "CreateUser");
                    }

                    status = MembershipCreateStatus.ProviderError;
                }
            }
            else
            {
                status = MembershipCreateStatus.DuplicateUserName;
            }

            return null;
        }

        private void WriteToEventLog(Exception ex, string action)
        {
            EventLog log = new EventLog();
            log.Source = EventSource;
            log.Log = EventLogName;

            string message = "An exception occurred communicating with the data source.\n\n";
            message += "Action: " + action + "\n\n";
            message += "Exception: " + ex.ToString();

            log.WriteEntry(message);
        }

        /// <summary>
        /// Converts a hexadecimal string to a byte array. Used to convert encryption
        /// key values from the configuration.
        /// </summary>
        /// <param name="hexString"></param>
        /// <returns></returns>
        private byte[] HexToByte(string hexString)
        {
            byte[] returnBytes = new byte[hexString.Length / 2];
            for (int i = 0; i < returnBytes.Length; i++)
                returnBytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
            return returnBytes;
        }

        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            bool result = false;
            try
            {
                result = _membershipService.DeleteUser(username, deleteAllRelatedData, HttpContext.Current.User.Identity.Name);
            }
            catch (Exception ex)
            {
                if (WriteExceptionsToEventLog)
                {
                    WriteToEventLog(ex, "DeleteUser");
                }
                else
                {
                    throw;
                }
            }
            return result;
        }

        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            var list = _membershipService.FindUsersByEmail(emailToMatch);
            totalRecords = list.Count;

            MembershipUserCollection result = new MembershipUserCollection();
            int limit = Math.Min(totalRecords, (pageIndex + 1) * pageSize);
            for (int i = pageIndex * pageSize; i < limit; i++)
            {
                result.Add(new VarMembershipUser(this.Name, list[i]));
            }

            return result;
        }

        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            var list = _membershipService.FindUsersByName(usernameToMatch);
            totalRecords = list.Count;

            MembershipUserCollection result = new MembershipUserCollection();
            int limit = Math.Min(totalRecords, (pageIndex + 1) * pageSize);
            for (int i = pageIndex * pageSize; i < limit; i++)
            {
                result.Add(new VarMembershipUser(this.Name, list[i]));
            }

            return result;
        }

        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            var list = _membershipService.GetAllUsers();
            totalRecords = list.Count;

            MembershipUserCollection result = new MembershipUserCollection();
            int limit = Math.Min(totalRecords, (pageIndex + 1) * pageSize);
            for (int i = pageIndex * pageSize; i < limit; i++)
            {
                result.Add(new VarMembershipUser(this.Name, list[i]));
            }

            return result;
        }

        public override int GetNumberOfUsersOnline()
        {
            return _membershipService.GetNumberOfUsersOnline(Membership.UserIsOnlineTimeWindow);
        }

        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            var userAccount = _membershipService.GetUserByName(username, userIsOnline);
            if (userAccount != null)
            {
                return new VarMembershipUser(this.Name, userAccount);
            }
            else
            {
                return null;
            }
        }

        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            var userAccount = _membershipService.GetUserByKey((Guid)providerUserKey, userIsOnline);
            if (userAccount != null)
            {
                return new VarMembershipUser(this.Name, userAccount);
            }
            else
            {
                return null;
            }
        }

        public override string GetUserNameByEmail(string email)
        {
            var user = _membershipService.GetUserByEmail(email);
            if (user != null)
            {
                return user.UserName;
            }
            return null;
        }

        public override bool UnlockUser(string userName)
        {
            return _membershipService.UnlockUser(userName);
        }

        public override void UpdateUser(MembershipUser user)
        {
            UserAccount userAccount = new UserAccount()
            {
                Id = (Guid)user.ProviderUserKey,
                UserName = user.UserName,
                Email = user.Email,
                IsApproved = user.IsApproved,
                IsLockedOut = user.IsLockedOut,
                FirstName = ((VarMembershipUser)user).FirstName,
                LastName = ((VarMembershipUser)user).LastName,
                ModifyBy = HttpContext.Current.User.Identity.Name,
                ModifyTime = DateTime.Now,
                IsStaff = ((VarMembershipUser)user).IsStaff
            };
            _membershipService.UpdateUser(userAccount);
        }

        public override bool ValidateUser(string username, string password)
        {
            bool isValid = false;

            password = EncodePassword(password);

            try
            {
                isValid = _membershipService.LoginUser(username, password, 
                    Membership.MaxInvalidPasswordAttempts, Membership.PasswordAttemptWindow);
            }
            catch (Exception ex)
            {
                if (WriteExceptionsToEventLog)
                {
                    WriteToEventLog(ex, "ValidateUser");
                }
                throw;
            }
            return isValid;
        }

        #region 密码操作

        /// <summary>
        /// 根据设定的密码格式，对明文的密码进行加密或散列
        /// </summary>
        /// <param name="password">密码明文</param>
        /// <returns>密码编码结果</returns>
        private string EncodePassword(string password)
        {
            string encodedPassword = password;

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Clear:
                    break;
                case MembershipPasswordFormat.Encrypted:
                    encodedPassword =
                      Convert.ToBase64String(EncryptPassword(Encoding.Unicode.GetBytes(password)));
                    break;
                case MembershipPasswordFormat.Hashed:
                    HMACSHA1 hash = new HMACSHA1();
                    hash.Key = HexToByte(_machineKey.ValidationKey);
                    encodedPassword =
                      Convert.ToBase64String(hash.ComputeHash(Encoding.Unicode.GetBytes(password)));
                    break;
                default:
                    throw new NotSupportedException("Unsupported password format.");
            }

            return encodedPassword;
        }

        /// <summary>
        /// Decrypts or leaves the password clear based on the PasswordFormat.
        /// </summary>
        /// <param name="encodedPassword"></param>
        /// <returns></returns>
        private string DecodePassword(string encodedPassword)
        {
            string password = encodedPassword;

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Clear:
                    break;
                case MembershipPasswordFormat.Encrypted:
                    password =
                      Encoding.Unicode.GetString(DecryptPassword(Convert.FromBase64String(password)));
                    break;
                case MembershipPasswordFormat.Hashed:
                    throw new ProviderException("Cannot unencode a hashed password.");
                default:
                    throw new ProviderException("Unsupported password format.");
            }

            return password;
        }

        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            oldPassword = EncodePassword(oldPassword);
            if (ValidateUser(username, oldPassword))
            {
                newPassword = EncodePassword(newPassword);
                return _membershipService.ChangePassword(username, newPassword, HttpContext.Current.User.Identity.Name);
            }
            return false;
        }

        public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
        {
            password = EncodePassword(password);
            if (ValidateUser(username, password))
            {
                return _membershipService.ChangePasswordQuestionAndAnswer(username, newPasswordQuestion, newPasswordAnswer, HttpContext.Current.User.Identity.Name);
            }
            return false;
        }

        /// <summary>
        /// 从数据源获取指定用户名所对应的密码
        /// </summary>
        /// <param name="username">为其检索密码的用户</param>
        /// <param name="answer">用户的密码提示问题答案</param>
        /// <returns>指定用户名所对应的密码</returns>
        public override string GetPassword(string username, string answer)
        {
            if (EnablePasswordRetrieval)
            {
                if (PasswordFormat == MembershipPasswordFormat.Hashed)
                {
                    throw new NotSupportedException("哈希密码不能检索");
                }

                if (RequiresQuestionAndAnswer)
                {
                    if (!_membershipService.ValidatePasswordAnswer(username, answer))
                        throw new MembershipPasswordException();
                }

                string result = _membershipService.GetPassword(username);
                result = DecodePassword(result);

                return result;
            }
            else
            {
                throw new NotSupportedException();
            }
        }

        /// <summary>
        /// 将用户密码重置为一个自动生成的新密码
        /// </summary>
        /// <param name="username">为其重置密码的用户</param>
        /// <param name="answer">指定的用户的密码提示问题答案</param>
        /// <returns>指定的用户的新密码</returns>
        public override string ResetPassword(string username, string answer)
        {
            if (EnablePasswordReset)
            {
                if (RequiresQuestionAndAnswer)
                {
                    if (!_membershipService.ValidatePasswordAnswer(username, answer))
                        throw new MembershipPasswordException();
                }
                string result = Membership.GeneratePassword(LenghtOfRandomPassword, MinRequiredNonAlphanumericCharacters);

                ValidatePasswordEventArgs args = new ValidatePasswordEventArgs(username, result, true);

                OnValidatingPassword(args);

                if (args.Cancel)
                {
                    if (args.FailureInformation != null)
                        throw args.FailureInformation;
                    else
                        throw new MembershipPasswordException("Reset password canceled due to password validation failure.");
                }
                else
                {
                    string persistentPassword = EncodePassword(result);

                    _membershipService.ChangePassword(username, persistentPassword, HttpContext.Current.User.Identity.Name);
                }

                return result;
            }
            else
            {
                throw new NotSupportedException();
            }
        }

        #endregion 密码操作

        #region 自定义方法

        /// <summary>
        /// 将某个用户加到一个应用，使其有访问该应用的权限
        /// </summary>
        /// <param name="userId">用户Id</param>
        /// <param name="appCode">应用代码</param>
        public void AddUserToApp(Guid userId, string appCode, string createBy)
        {
            _membershipService.AddUserToApp(userId, appCode, createBy);
        }

        #endregion 自定义方法
    }
}
